Repository navigation
Conversation
Empty tracking commit for the implementation work tracked in: docs/superpowers/plans/2026-06-27-provision-local.md Issues: - Epic: <epic-issue-url> - Per-section sub-issues linked from the epic. This PR opens as a DRAFT and stays draft until Section 1 lands its first real commit. Each section opens as its own follow-up PR that lands here before the umbrella merges to main.
run_shared_checks iterates every declared adapter and dispatches validate_adapter_manifest, which for Spin does fs::read_to_string(manifest_root.join(rel)). With the containment guard sitting after run_shared_checks, a manifest declaring [adapters.spin.adapter].manifest = "../outside/spin.toml" could trigger a filesystem read outside the project root before the guard rejected it — a spec violation of §"Path containment (MUST)" which requires the helper run BEFORE any manifest-path use. Fix by relocating the check to fire immediately after load_push_context, and looping over every declared adapter (not just ctx.adapter) since run_shared_checks reads all of them. Also close Task 7's Minor about the duplicate adapter_entry call by removing the now-redundant per-adapter guard block. Regression test: config_push_local_rejects_parent_traversal_in_ sibling_spin_adapter declares a poisoned Spin adapter alongside the pushed axum adapter, and asserts the error names the containment violation (not Spin's "failed to read spin manifest" message that would surface under the old ordering). Also tighten copy_tree's else-branch to explicitly gate on is_regular_file() rather than "everything non-dir non-symlink", add a Unix symlink-skip test, and drop a stale #[expect(dead_code)] on ValidationContext::manifest_path that now has real callers.
…to feature/provision-local-impl
The bare-cwd variant of the accept-test (--manifest edgezero.toml)
previously wrote the manifest to a tempdir and set EDGEZERO_MANIFEST,
but run_provision reads args.manifest directly (no env fallback).
The test therefore failed on manifest load ("failed to load
edgezero.toml") and its negative !contains(path-safety-markers)
assertion vacuously passed — no actual coverage of the
`args.manifest.parent() == ""` fallback.
Fix by adding a CwdGuard RAII helper that chdirs into the tempdir
under the manifest_guard() serialisation lock and restores the
previous cwd on drop. Both accept-tests now also assert positively
that the error is the (true, true) dispatch stub ("local dry-run
staging lands in Task 10/11"), proving the manifest loaded AND
path-safety passed AND we reached the dispatch matrix. Drop the
now-unnecessary EnvOverride from both tests.
Reviewer: reviewer of Task 9 pushed this as a Low ahead of Task 10
because run_with_staging depends on manifest-root/cwd correctness.
…to feature/provision-local-impl
The dependabot syn 2->3 update refreshed the ROOT Cargo.lock but not the excluded `examples/app-demo` workspace's lock, so its CI step (`cargo test --workspace --all-targets --locked`) failed with 'cannot update the lock file ... --locked was passed'. Regenerate app-demo's lock (adds syn 3.0.3) so it matches its manifests; all app-demo tests pass under --locked.
…ules Reconciles the branch with origin/main (Fastly chunked-config GC feature #314 plus dependency bumps). The main feature — operator-invoked `config gc` that reclaims orphaned Fastly config-store chunk entries — was added to the monolithic `cli.rs` on main, while this branch had split that file into a `cli/` module directory (matching every other adapter). The feature is ported into the split structure: - cli/gc.rs (new): the reclamation core (list/classify/plan/execute, generation proofs, RFC3339 age gating, recovery-command reporting). - cli/mod.rs: the `gc_config_entries` + `preflight_config_write` trait methods and the shared manifest-lock / atomic-replace / key-hygiene guards. - push_cloud / push_local / provision_local / provision_cloud: main's GC write-path changes (gc_roots tracking, orphan prune, listing-based fail-closed reads, manifest locking) hand-merged onto the branch's versions, PRESERVING the branch fixes (reserved runtime-env store guard, child-reaping stdin writes, "wrote local ..." status verbs, the not-yet-provisioned refusal, the 255-char key limit). - chunked_config.rs taken from main (GC exports + 255 limit). - The other adapters (axum/cloudflare/spin) get explicit no-op `gc_config_entries`/`preflight_config_write` impls for the `missing_trait_methods` lint. - Two main GC test fixtures updated to the branch's "manifest resolves inside the adapter crate" layout; the malformed-TOML dry-run case (which the branch deliberately refuses, consistent with the real push) dropped from the "reaches the writer" set. - app-demo Cargo.lock synced for the syn 3 bump. All gates green: fmt, clippy --all-features -D warnings, workspace tests, feature check, spin wasm32, app-demo --locked, docs build/format/lint, shellcheck, smoke backup tests. Fastly cli 252 tests, edgezero-cli 302.
The merge auto-merged `examples/app-demo/Cargo.lock` into a state cargo
could not parse ("failed to parse lock file"), because origin/main's GC
feature adds `rusqlite` (and siblings) to that excluded workspace and the
three-way lock merge left it inconsistent. Regenerate it so
`cargo test --locked` parses and resolves cleanly.
The ported fastly `cli` test modules import/use `#[cfg(unix)]` fixtures
(fake-`fastly` PATH shims, `path_mutation_guard`) whose imports were left
ungated. On the host (unix) everything compiles, but the CI `fastly wasm
clippy` matrix (`--features "fastly cli"` and `--features fastly` on
wasm32-wasip1, which is NOT unix, with --all-targets) saw the cfg(unix)
helpers vanish and the imports/one test dangle -- 9 unresolved/unused-import
errors. Align the `#[cfg(unix)]` gating on those imports (and the one test
that calls `path_mutation_guard`) so the import set matches what compiles on
a non-unix target. No imports deleted; each stays available on unix.
Verified clean: fastly clippy on {fastly cli, fastly} x wasm32-wasip1, cli
+ no-default on host; fastly tests (252 cli / 53 default); plus cloudflare
+ spin wasm clippy and app-demo fmt/clippy.
…ects Two P0 blockers (both live-verified): - Spin build no longer TRUNCATES its own artifact. `refresh_declared_source` compared lexical paths, so the default `source` (`../../target/.../x.wasm`, a relative path with `..`) never matched the normalised artifact and `fs::copy` copied the file onto itself -> 0 bytes. Now guarded by a canonicalize-based same-file check (also for the conventional refresh), plus containment (refuse a `source` resolving outside the workspace or through a symlink) and single-component matching (#1, #5). - Fresh Cloudflare scaffolds can build/serve/deploy. The synthesised `wrangler.toml` now declares `[build] command = "worker-build --release"` (main points at the worker-build-generated shim), and the adapter drops its `[commands]` shell block so build/serve/deploy route through the registry dispatch, which passes `--config <manifest>` (also fixes custom manifest paths being ignored) (#2, #7). Spin: - `[adapters.spin.adapter].cloud = true` is the SOLE Fermyon Cloud selector; the legacy deploy-command heuristic and its `manifest_adapter_deploy_cmd` field are removed, and `config diff` threads the flag like `config push` (#3). - `edgezero serve` forwards parent-shell `EDGEZERO__*` overrides to the guest via `spin up --env` (highest precedence) so a shell override reaches the sandboxed component (#4). - deploy uses `spin cloud deploy` (bare `spin deploy` needs a plugin on Spin 3+/4) (#6). Cloudflare / Axum (via subagents): - typed secret keys are validated for `.dev.vars` round-trip (empty/=/#/ newline/whitespace) (#16); local push refuses a `.wrangler` symlink escape (#11); axum runtime honours `EDGEZERO_MANIFEST` when locating `.edgezero` (#8). Fastly: - cloud provision now writes the per-store `__NAME` runtime mappings into `edgezero_runtime_env` so a renamed cloud store is reachable (#10); the dry-run preflight redacts malformed-manifest parse errors and models the real writer's inline-toml rejection (#13). Convergence (via subagent): reprovisioning converges managed `__NAME` overlays across env_file + all four adapters via a new `upsert_lines_with_header` (#9). CLI: - dry-run staging refuses a TMPDIR inside the project (self-recursive copy) and no longer follows a symlinked live path into the diff output (#12); - cloud provision preflights manifest writability before mutating the account, so an unwritable manifest can't orphan a created resource (#15).
R23 fixed a three-level composed-deploy DEADLOCK by making a borrowing deploy hold NO lock -- but a borrowing deploy DOES write `fastly.toml` (`fastly compute deploy` records the service_id), so a lock-free deploy borrower races a co-sibling provision and loses an edit. Replace the single shared `provision.borrow.lock` with a DEPTH-KEYED sibling lock: a borrower serialises its file writes against its co-siblings on `provision.borrow.<depth>.lock`, and a deploy advertises `depth + 1` to its subprocess (`EDGEZERO_PROVISION_SIBLING_DEPTH`). A more-nested borrower therefore serialises on the NEXT depth's file and never blocks on an ancestor's sibling lock -- holding serialisation at EVERY level with no deadlock, and making borrowing deploys serialise like provisions. (#14)
…tainment refinements - cloudflare: run deploy/serve from the crate dir with an absolute --config and a depth-aware `main` relpath so a nested wrangler config resolves the worker-build shim correctly - spin: honour the manifest's adapter component selector when refreshing the declared source, reject intermediate symlinks + self-copy, and forward the guest --env overlay without panicking on non-Unicode env keys; deploy via `spin cloud deploy` - axum: absolute()-normalise before .parent() in manifest_root_from_env - fastly: converge every store's runtime name mapping on cloud provision; local push/provision dry-run degrades over unprovisioned/malformed prior state instead of hard-failing (matches the chunked-config GC contract) - provision lock: compare advertised vs calculated lock targets by canonical path, and re-authenticate after a sibling borrow so a stale token can't bypass an unrelated holder - provision dry-run: diff the "before" side against a T1 staging snapshot instead of re-reading the live worktree at render time, so a concurrent external rewrite can't smear two worktree instants into one report; redaction now tracks multiline quoted env values - docs: cloudflare required [build], cloud=true selector, `spin cloud deploy`, fastly secrets via env var - smoke scripts: share a single backup/provision-local helper
Integrates upstream's layered deploy actions + Fastly staging lifecycle (#316), optional typed secret paths + Fastly store mappings (#344), runtime_env_config for custom Fastly entry points (#351), and dependency bumps, reconciled against this branch's provision-local work. Conflict resolutions: - adapters (cloudflare/spin/axum): added the 4 Fastly-only staging-action reject arms (DeployStaged/EmitVersion/Healthcheck/Rollback) to each `cli/mod.rs`; kept our `cli/` module restructure over upstream's monolithic `cli.rs`. - fastly: re-homed upstream's ~4300-line staging lifecycle into a new `cli/staging.rs` wired to the 4 execute arms, reusing our existing module helpers (RUNTIME_ENV_STORE_NAME, ConfigStoreLookup, populate_runtime_name_mappings, find_fastly_manifest, ...) instead of duplicating them. - adapter.rs: merged our `build_child_env` (provision-lock advertisement + required-secrets assertion + env overlay) into upstream's `build_shell_command`/`run_shell`/`run_shell_tee` split; `execute_capture` is now overlay-aware. - lib.rs: `run_deploy` now acquires the deploy provision lock and threads its advertisement overlay through upstream's staged/capture/production dispatch; kept upstream's new run_healthcheck/run_rollback/ run_active_version. Extracted `acquire_deploy_lock`. - config.rs: kept our read-phase lock release before the consent prompt and upstream's emit-outputs-on-skip; unioned ConfigPushArgs bools (added `staging`). Extracted `assert_push_adapter_paths`. - args/main/template: merged the `--staging`/`Rollback` additions with our `--local`/typed-provision changes. Verified: fmt, clippy --workspace --all-targets --all-features, workspace tests, feature check, spin wasm32-wasip2, app-demo --locked, docs lint, shellcheck.
`run_with_staging_refuses_a_tempdir_inside_the_project` overrode the process-global `TMPDIR` while holding `env_lock()` -- a DIFFERENT mutex from the `manifest_guard()` that the other ~93 env-mutating tests in the `edgezero-cli` binary take. Two disjoint mutexes over one global environment serialise nothing. `tempfile::TempDir::new()` reads `TMPDIR`, so concurrent tests had their tempdirs created inside this test's `inside-tmp` directory, which it then deleted on drop. The victim failed with a confusing `No such file or directory (os error 2)` on a path like `.../<outer>/inside-tmp/<inner>/edgezero.toml`, and because it panicked while holding `manifest_guard`, it POISONED that mutex -- cascading into 30-47 collateral `manifest guard: PoisonError` failures that disguised the single root cause. Take `manifest_guard()` instead, per that guard's documented contract. Also correct `env_lock`'s doc: "one lock per test binary is sufficient" is only true when a binary uses ONE lock. `edgezero-cli` defines three, so the guidance as written invited exactly this bug. Verified: reproduced at ~1/10 stress runs (16 threads) before the change, including a captured leaked-tempdir path; 0/10 after. Full workspace tests 4x (20/20 binaries, no panics), fmt, clippy --workspace --all-targets --all-features, feature check, spin wasm32-wasip2, shellcheck.
…ing the smoke fixture Three independent CI failures on the merge commit, none caught locally because the gate set I had been running was incomplete. 1. `fastly wasm clippy` (format.yml matrix): `unused import: tempfile::tempdir` in cli/staging.rs. Every `tempdir()` call site is inside a `#[cfg(unix)]` test that PATH-shims a fake `fastly`, so the import is dead on wasm32-wasip1. Gate it with `#[cfg(unix)]` like its sibling imports. A host-target `clippy --all-features` never compiles this target, which is why it passed locally. 2. `lifecycle-smoke` (deploy-action.yml): a staged deploy against a COMMANDS-ONLY adapter was rejected with "`[adapters.fastly.adapter] .manifest` is not declared". `assert_adapter_declaration_consistent` deliberately permits an adapter that declares NEITHER `.manifest` nor `.crate` (it owns its build/deploy and never uses declared-path discovery), but `resolve_declared_adapter_paths` hard-errored on the missing `.manifest`. A `deploy --staging` bypasses `commands.<action>` and drives the vendor CLI, so it reaches that resolver. Return `(None, None)` for the neither-declared case -- exactly as for an undeclared adapter -- so the adapter falls back to its own discovery. Half-declared adapters are still rejected. 3. `cache-smoke` / `config-push-smoke` (deploy-action.yml): "committed source is required; the working tree for 'fixture-app' is dirty" on the SECOND deploy. `run_deploy` now holds the cross-process provision lock, which creates `<project>/.edgezero/provision.lock`. Every real project gitignores the whole `.edgezero/` directory (this repo's .gitignore and the scaffolder's gitignore.hbs both do), but the smoke fixture predates the lock and hand-rolls a minimal ignore list, so the lock file showed up as untracked. Add `.edgezero/` to the fixture's .gitignore. The `cargo test` job's failure was the TMPDIR guard leak already fixed in 022b933 (its log shows the same `inside-tmp` leaked path); no further change needed here. Verified: deploy-core contract tests 260/260, smoke_backup_test 37/37, workspace tests 20/20 binaries, full wasm matrix (fastly wasip1 with `fastly` and `fastly cli`, cloudflare wasm32-unknown-unknown, spin wasip2), fastly cli-only and no-default-features clippy, check_no_nested_app_config, app-demo fmt + clippy --locked, workspace fmt + clippy --all-features, shellcheck.
Let adapters classify sensitive TOML artifacts for fail-closed preview redaction, and omit source excerpts from native provision parse errors. Use the existing Fastly manifest lock and atomic replacement for both base and typed local provision so deploy writeback cannot be erased. Normalize child application manifest selectors before changing cwd, accept root adapter crates, and guard Spin artifact and SQLite paths. Cover the reported failures with secret-sentinel, subprocess locking, child execution, and public adapter symlink regressions.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
📝 PR Review
Reviewed dac4838544eea394d0f3ddd870d90e3c10ebd299 against local main (fc4ccba0ee57). The adapter-owned dispatch and shared local provisioning design have substantial regression coverage, but the reproduced containment, confidentiality, feature-build, and integration defects below need correction. Findings 1–10 and 12–14 are inline.
Blocking cross-cutting finding
🔧 [11] Migrate Spin cloud consumers to the explicit selector
Locations: docs/guide/cli-walkthrough.md:345 and scripts/smoke_test_config_key_override.sh:631–644.
Cloud config dispatch now requires [adapters.spin.adapter].cloud = true; the old deploy-command heuristic is no longer consulted. Neither the walkthrough's cloud flow nor the smoke's demo manifest sets that selector. For applications declaring config stores, the walkthrough therefore pushes to local SQLite before deploying to Cloud. The “Cloud Unsupported” smoke likewise targets local state, or accepts a missing-manifest error as its expected unsupported result. Its push occurs after the per-adapter restoration.
Fix: document setting cloud = true before the cloud push, replace the walkthrough's remaining deploy-command heuristic description, and run the cloud smoke with an isolated, explicitly cloud-selected manifest.
Praise
😃 [15] Strong preservation coverage
crates/edgezero-adapter/src/env_file.rs:455–475 checks exact bytes across multiline quote delimiters, CRLF, managed-looking assignments inside values, and missing final newlines. These tests directly protect against secret-file corruption.
Checks
📝 Required/local gates
- Formatting: PASS
- All-targets, all-features workspace Clippy with
-D warnings: PASS - Workspace/all-targets tests: PASS
- Combined Fastly/Cloudflare/Spin feature compilation: PASS
- Spin
wasm32-wasip2compilation: PASS - Diff whitespace check: PASS
📝 Additional checks
- Standalone audit-feature compilation: FAIL, as described in finding 4.
- Minimal-feature Clippy: FAIL (unused CLI imports and an unfulfilled lint expectation).
- Focused scratch probes reproduced the safety issues, Cargo-resolution mismatches, auth prerequisite regression, Wrangler bulk-operation ambiguity, Spin identifier rejection, output-error handling, and descendant-survival behavior.
- Windows, authenticated cloud operations, full emulator smoke suites, and the ignored generated-project build test remain unverified.
- No repository source files were modified during review.
📝 Totals: 13 🔧 required changes, 1 ♻️ non-blocking suggestion, 1 😃 praise item.
| fs::create_dir_all(parent) | ||
| .map_err(|err| format!("failed to create {}: {err}", parent.display()))?; | ||
| } | ||
| fs::copy(artifact, &src_path).map_err(|err| { |
There was a problem hiding this comment.
🔧 [1] Write to the destination that passed the containment check
reject_symlink_components checks normalized, but create_dir_all and fs::copy operate on src_path. For source = "escaped/../victim.wasm", where escaped links to an external directory's child, normalization checks an in-project path while the actual filesystem traversal writes the external victim.wasm. A scratch CLI build with a successful fake Cargo command reproduced the external overwrite.
Fix: perform filesystem operations on the exact guarded, normalized destination, or reject symlinks in the original path before collapsing parent components. Add a symlink-plus-.. regression.
| let snapshot_id = manifest_id(&snapshot)?; | ||
| let startup = tempfile::tempdir().map_err(|err| err.to_string())?; | ||
| // Fastly reads this before parsing --dir/--env and may upgrade it. | ||
| fs::write(startup.path().join("fastly.toml"), &snapshot).map_err(|err| err.to_string())?; |
There was a problem hiding this comment.
🔧 [2] Keep the private startup manifest owner-only
This copies the complete source manifest, including inline secret data, without preserving its access restrictions. With umask 022, I reproduced a 0600 source becoming a 0644 startup copy inside a 0755 temporary directory during native deployment. Other local users can read that copy. The separate NamedTempFile does not protect this startup file.
Fix: create the startup directory with owner-only permissions (0700 on Unix), or create its manifest as 0600 from the outset. Add a regression using a restricted source and umask 022.
| if let Some(dir) = target_dir_from_args(build_args) { | ||
| return CargoTargetDir::Explicit(resolve_dir_against(crate_dir, &dir)); | ||
| } | ||
| if let Some(dir) = config_target_dir_from_args(crate_dir, build_args) { |
There was a problem hiding this comment.
🔧 [3] Match Cargo's target-directory resolution before selecting artifacts
Two verified cases disagree with Cargo:
- With
CARGO_TARGET_DIR=env-dirand--config 'build.target-dir="arg-dir"', Cargo builds intoenv-dir, but this resolver selectsarg-dir. - The loops at lines 191–203 consult
.cargo/config.tomlwhen the extensionless.cargo/configexists but has no target directory. Cargo ignores that sibling file entirely. This also affects theCARGO_HOMElookup.
The adapter artifact lookups trust this result exclusively, so a successful build can fail discovery or package a stale artifact from the incorrectly selected directory. Scratch Cargo metadata/build probes confirmed both mismatches.
Fix: check CARGO_TARGET_DIR after --target-dir but before --config; keep CARGO_BUILD_TARGET_DIR below --config. Select one configuration filename per directory before inspecting its contents, then continue to ancestors/global configuration when that selected file supplies no target directory.
| @@ -53,6 +52,7 @@ use std::process; | |||
| use std::result::Result; | |||
| use std::string::ToString; | |||
|
|
|||
| use edgezero_cli::stream::{info_line, stdout_line}; | |||
There was a problem hiding this comment.
🔧 [4] Preserve standalone audit-feature compilation
edgezero_cli::stream is gated behind cli, but this binary requires only nested-app-config-check. The following previously supported feature combination now fails with E0432:
cargo check -p edgezero-cli --bin check_no_nested_app_config \
--no-default-features --features nested-app-config-checkFix: expose stream when either cli or nested-app-config-check is enabled, and add this focused feature-combination check. The all-features gate cannot detect this regression.
| // env the shell path above would have applied, otherwise a `serve` | ||
| // here starts with none of the `.env` secrets and resolves its | ||
| // manifest from the process cwd. | ||
| with_exec_context(adapter_name, manifest_loader, env_overlay, |ctx| { |
There was a problem hiding this comment.
🔧 [5] Do not require application runtime prerequisites for authentication
The registry-backed auth path now uses with_exec_context, which validates required application secrets and resolves adapter paths before invoking login/logout/status. Authentication does not require those runtime secrets or application files. I reproduced edgezero auth status --adapter axum failing solely because an application secret was unset; the same dispatch also serves vendor authentication. Partial path declarations are rejected by the unconditional check above as well.
Fix: make secret/path prerequisites action-aware. Built-in authentication should be able to run before application runtime configuration is complete. Preserve the resolved child environment without imposing unrelated build/serve prerequisites.
| smoke_require_port_free "$PORT" | ||
| # `EnvSecretStore` resolves the `demo_api_token` key verbatim from the | ||
| # process env, so /config/typed's secret walk needs it exported. | ||
| (cd "$DEMO_DIR" && demo_api_token="$DEMO_SECRET_VALUE" \ |
There was a problem hiding this comment.
🔧 [9] Export the secret variable the Axum reader actually uses
The newly selected /config/typed endpoint requires api_token, whose configured key is demo_api_token. EnvSecretStore looks that up as EDGEZERO__SECRETS__DEMO_API_TOKEN, not the bare demo_api_token exported here. In a clean environment the endpoint therefore fails secret resolution rather than returning the expected greeting.
Fix: launch with:
EDGEZERO__SECRETS__DEMO_API_TOKEN="$DEMO_SECRET_VALUE"The key-override smoke already uses the correctly prefixed namespace.
| fi | ||
| done | ||
| fi | ||
| pkill -TERM -P "$pid" 2>/dev/null || true |
There was a problem hiding this comment.
🔧 [10] Stop all owned descendants before restoring state
pkill -P "$pid" terminates only direct children. Grandchildren are handled only when lsof finds them holding the selected port, so without lsof, or for non-listening descendants, cleanup returns while they remain alive. Callers then restore operator files underneath surviving writers.
I reproduced a wrapper → shell → Node tree where the grandchild survived smoke_stop_server, then changed the restored state from original to survivor-flush.
Fix: capture the complete owned descendant tree before terminating ancestors, terminate and wait for those descendants, and only then permit backup restoration. Port ownership alone is insufficient.
| Some("has leading or trailing whitespace") | ||
| } else if key.starts_with('#') { | ||
| Some("starts with `#`, which `.env` readers treat as a comment") | ||
| } else if key.to_ascii_uppercase().starts_with(RESERVED_PREFIX) { |
There was a problem hiding this comment.
🔧 [12] Validate the derived Axum secret name, not a raw reserved prefix
A raw key such as EDGEZERO__ADAPTER__HOST cannot collide with the runtime host setting: both provisioning and EnvSecretStore map it to EDGEZERO__SECRETS__EDGEZERO__ADAPTER__HOST. A current-source probe successfully read that credential without setting adapter-host configuration, while this typed validation rejected the same key.
This blocks previously supported configurations during typed validation, provisioning, push, and diff.
Fix: remove the raw-key EDGEZERO__ prefix rejection. Retain the shared character-set and derived-name collision checks, and replace the rejection test with a round-trip test.
|
|
||
| `config push` still never writes secret **values** — the blob stores each | ||
| `#[secret]` field's key NAME, not its resolved value. Set the value at run time | ||
| via `SPIN_VARIABLE_<NAME>=<value>` (or `spin up --env <NAME>=<value>`). |
There was a problem hiding this comment.
🔧 [13] Supply a Spin application variable, not a component environment variable
spin up --env NAME=value sets a component's WASI environment. It does not supply the application variable referenced by [component.<id>.variables] and read through spin_sdk::variables by SpinSecretStore. The generated secret therefore remains at its empty default when the documented alternative is used.
Fix: retain SPIN_VARIABLE_<NAME>=<value>, or document Spin's --variable name=value option instead of --env.
| [adapters.cloudflare.adapter] | ||
| crate = "crates/cf" | ||
| manifest = "crates/cf/wrangler.toml" | ||
| deployed = { kv_namespaces = { sessions = "old-id" } } |
There was a problem hiding this comment.
♻️ [14] Make the inline-table fixtures exercise existing inline state
This deployed value is still inside [adapters.cloudflare.adapter], not [adapters.cloudflare]. Similarly, the fixture at line 4224 puts its supposedly root-level adapters value under [app]. Parsing the current fixtures confirms both intended writeback locations are absent before the operation, so the tests create missing tables rather than update existing inline tables.
Suggestion: move deployed into the adapter table and place the root inline adapters assignment before [app]. Assert the intended inline tables exist before invoking writeback. This is non-blocking test-fidelity feedback.
Use the guarded Spin copy destination and private Fastly startup directories. Match Cargo target resolution, select Wrangler namespaces explicitly, and validate Spin component IDs independently of Cargo artifact names. Preserve Axum secrets whose derived environment names do not collide with config. Separate authentication from runtime prerequisites and propagate payload output errors through diff, push and audit results. Preserve audit-only and minimal-feature builds. Correct inline-table and TOML preview coverage. Stop all captured smoke descendants before restoring state, isolate explicit Spin cloud smoke manifests, and fix documented secret setup. Add focused regressions and CI checks for the reported failures.
Adds
edgezero provision --local, folds all five adapter manifests into the gitignored-generated model, and hardens the surrounding CLI (path safety, env redaction, adapter-scoped env-file load) acrossprovision,config push,config diff, andserve.Source artifacts
docs/superpowers/specs/2026-06-23-provision-local.mddocs/superpowers/plans/2026-06-27-provision-local.md— 43 tasks across 9 sectionsThe 9 plan sections all landed on this branch (rather than as separate sub-PRs merging in); each closes on merge below.
What ships
provision --local. NewProvisionMode::Localarm threads throughAdapter::provision. Local mode:toml_edit::DocumentMut(CLI-owned bootstrap runs before validation).EDGEZERO__STORES__<KIND>__<ID>__NAME=…) so the runtime resolves stores at startup..edgezero/.env,.dev.vars,<spin_crate>/.env).Dry-run stages a real
fs::copyinto atempfile::TempDirand diffs the result back. The tree stays byte-identical either way.Typed provision (
run_provision_typed::<C>). Generated<app>-clibinaries dispatch the typed variant, which runs the base flow then additionally emits per-secret placeholder lines derived fromAppConfig's#[secret]/#[secret(store_ref)]fields (Axum:<key>=; Cloudflare:<key>=""in.dev.vars; Fastly:[[local_server.secret_stores.<store_id>]]; Spin: lowercased[variables]+SPIN_VARIABLE_<NAME>=in.env). The bundlededgezerobinary intentionally does not emit placeholders — it has no downstreamAppConfigtype.All five adapter manifests are provision-generated and gitignored.
axum.tomljoinedwrangler.toml/fastly.toml/spin.toml/runtime-config.tomlin the 2026-07 amendment; the scaffold.hbstemplate foraxum.tomlwas removed so scaffold-time provision is the single writer. A CI grep gate enforces the whole set.Path safety. New
path_safety::{assert_provision_paths_safe, assert_provision_paths_contained}helpers guard every CLI entry point that joinsmanifest_rootwith an operator-declared adapter path. Cloud dispatch runs the "safe" variant (absolute-path rejection +..traversal rejection);--localruns the stricter "contained" variant that additionally requires BOTH[adapters.<name>.adapter].manifestAND[adapters.<name>.adapter].crateto be declared, with the manifest resolving inside the crate dir. Wired intorun_provision,run_provision_typed,run_config_push_typed,run_config_diff_typed, andrun_serve.Renamed / nested adapter crate support.
cli_support::read_adapter_crate_namewalks upward from the manifest's parent to the first Cargo.toml insidemanifest_rootand reads[package].name. All four bundled adapter synthesisers use this to name generated manifests correctly under operator renames ([adapters.axum.adapter].crate = "crates/server"→crate = "server"inaxum.toml; Cargo packagespin-server→ wasm pathspin_server.wasm). Nested manifests likecrates/server/config/spin.tomlresolve correctly too.Spin component / crate decoupling.
synthesise_spin_tomlnow takes two distinct identities —crate_name(from Cargo.toml, drives[application].nameand the wasm source basename) andcomponent(from[adapters.spin.adapter].component, drives[[trigger.http]].componentand the[component.<id>]table key). Fixes the pre-2026-07 conflation where setting a component selector would silently mispoint the wasm artifact.Env-value redaction in dry-run.
.envand.dev.varsbodies are rewritten asKEY=<redacted>before diffing so operator secrets never surface in dry-run output. CommentedKEY=valuelines (adapter provisioners emit these as# EDGEZERO__STORES__…__KEY=placeholders, and operators stash real values behind#for later use) get the same treatment; pure comments and blank lines pass through so structural drift stays readable.Adapter-scoped env-file load in
run_serve. Axum reads<manifest_root>/.edgezero/.env; Spin reads.envnext to the resolvedspin.toml(matches where provision writes it — a nestedmanifest = "crates/spin/config/spin.toml"correctly resolves tocrates/spin/config/.env). Both run through the path-safety guard before the read. Contract test spawns a real serve child, inspects its inherited environment via a file the child writes, and asserts the marker propagates.Section breakdown
Each section closes on merge:
ManifestAdapterDeployedschema + writeback (Tasks 14–16, 16b–16c)Post-plan additions (from external review iterations)
Multiple review passes drove refinements past the original plan:
.gitignore+ CI gate + spec-plan-guide docs updated.read_adapter_crate_nameupward walk added to support nested manifests (v5 review).[application].nameand wasm basename decoupled from the component selector (v5 review)..manifestAND.crate;run_config_diff_typedgets the guard (previously read-only justified skipping);run_servegets the guard on the Spin env-file load.# KEY=valuelines (v6/v7 reviews).run_manifest_shape_gates(addsvalidate_deployed_field_ownershipalongside capability + handler-path checks).run_servederives.envpath from[adapters.spin.adapter].manifest.parent()(was.crate— broke on nested manifests). Contract test observes the spawned child's env (v8 review).#[expect(...)]restructure sweep acrossedgezero-cli,edgezero-core/manifest.rs(Deserialize impls), and adaptercli/mod.rsfiles (removed via explicit trait method overrides,ConfigPushSuppressionssub-struct,streammodule, structural code fixes rather than workspace-levelallows).CI gates (all pass locally)
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-targetscargo check --workspace --all-targets --features "fastly cloudflare spin"cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spinexamples/app-demoworkspace testscargo test -p edgezero-cli --test generated_project_builds -- --ignored)Test plan
provision_local_*cases + Spin's env-label alignment quartet (Section 9)run_serve,run_config_diff_typed,run_config_push_typed,run_provisionMigration notes
<app>-cli provision --adapter <name> --localafter cloning to regenerate the five adapter manifests (.gitignorecovers all of them plus.dev.vars).[adapters.<name>.adapter].manifestand[adapters.<name>.adapter].crateare now both required forprovision --localandconfig push/diff --local. Every scaffolded project already sets both. Cloud dispatch remains permissive.run_provision_typed::<AppConfig>(not the untypedrun_provision) so#[secret]fields reach the adapters'provision_typedimpls.